﻿using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Security;
using ForumEngine.Security;
using ObjectModel;

namespace ViewServices.Security
{
    public class CustomMembershipProvider : MembershipProvider
    {
        private bool _enablePasswordRetrieval;
        private bool _enablePasswordReset;
        private bool _requiresQuestionAndAnswer;
        private bool _requiresUniqueEmail;
        private int _maxInvalidPasswordAttempts;
        private int _passwordAttemptWindow;
        private MembershipPasswordFormat _passwordFormat;
        private int _minRequiredPasswordLength;
        private int _minRequiredNonAlphanumericCharacters;
        private string _passwordStrengthRegularExpression;

        private const int SaltLength = 16;

        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            base.Initialize(name, config);

            _enablePasswordRetrieval = ParameterHelper.GetBooleanValue(config, "enablePasswordRetrieval", false);
            _enablePasswordReset = ParameterHelper.GetBooleanValue(config, "enablePasswordReset", true);
            _requiresQuestionAndAnswer = ParameterHelper.GetBooleanValue(config, "requiresQuestionAndAnswer", true);
            _requiresUniqueEmail = ParameterHelper.GetBooleanValue(config, "requiresUniqueEmail", true);
            _maxInvalidPasswordAttempts = ParameterHelper.GetIntValue(config, "maxInvalidPasswordAttempts", 0, 5, false);
            _passwordAttemptWindow = ParameterHelper.GetIntValue(config, "passwordAttemptWindow", 0, 10, false);
            _passwordFormat = ParameterHelper.GetEnumValue(config, "passwordFormat", MembershipPasswordFormat.Hashed);
            _minRequiredPasswordLength = ParameterHelper.GetIntValue(config, "minRequiredPasswordLength", 0, 20, false);
            _minRequiredNonAlphanumericCharacters = ParameterHelper.GetIntValue(config, "minRequiredNonAlphanumericCharacters", 0, 10, false);
            _passwordStrengthRegularExpression = config["passwordStrengthRegularExpression"];

            if (!string.IsNullOrEmpty(_passwordStrengthRegularExpression))
            {
                _passwordStrengthRegularExpression = _passwordStrengthRegularExpression.Trim();

                if (!string.IsNullOrEmpty(_passwordStrengthRegularExpression))
                {
                    new Regex(_passwordStrengthRegularExpression);
                }
            }

            if (_minRequiredPasswordLength != 0 && _minRequiredPasswordLength < 4)
            {
                _minRequiredPasswordLength = 4;
            }

            if (PasswordFormat == MembershipPasswordFormat.Hashed && EnablePasswordRetrieval)
            {
                throw new NotSupportedException("Cannot retrieve password for hashed password format");
            }
        }

        private void ValidatePassword(string password)
        {
            if (MinRequiredPasswordLength > 0 && password.Length < MinRequiredPasswordLength)
            {
                throw new NotSupportedException("Password length must be minimum " + MinRequiredPasswordLength + " characters");
            }

            if (MinRequiredNonAlphanumericCharacters > 0 && password.Count(char.IsLetterOrDigit) < MinRequiredNonAlphanumericCharacters)
            {
                throw new NotSupportedException("Password must contain at least " + MinRequiredNonAlphanumericCharacters + " non alphanumeric characters");
            }

            if (!string.IsNullOrEmpty(PasswordStrengthRegularExpression) && !Regex.IsMatch(password, PasswordStrengthRegularExpression))
            {
                throw new NotSupportedException("Password must conform to the specified rules");
            }
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            if (providerUserKey != null && !(providerUserKey is Guid))
            {
                throw new ArgumentException("Invalid provider user key", "providerUserKey");
            }

            if (!string.IsNullOrEmpty(username))
            {
                username = username.Trim();
            }

            if (!string.IsNullOrEmpty(password))
            {
                password = password.Trim();
            }

            if (string.IsNullOrEmpty(username))
            {
                throw new ArgumentNullException("username");
            }

            if (string.IsNullOrEmpty(password))
            {
                throw new ArgumentNullException("password");
            }

            if (RequiresUniqueEmail && string.IsNullOrEmpty(email))
            {
                throw new ArgumentNullException("email");
            }

            username = username.Trim();

            password = password.Trim();

            ValidatePassword(password);

            using (var domainModel = new DataAccess.DomainModel())
            {
                var lowerUserName = username.ToLower();

                if (domainModel.Users.Any(u => username.ToLower() == lowerUserName))
                {
                    status = MembershipCreateStatus.DuplicateUserName;
                    return null;
                }

                var user = new User
                {
                    Name = username,
                    Email = email,
                    Password = password,
                    PasswordQuestion = passwordQuestion,
                    PasswordAnswer = passwordAnswer,
                    IsApproved = isApproved,
                    Created = DateTime.UtcNow,
                    ProviderUserKey = providerUserKey != null ? (Guid?) providerUserKey : null
                };

                EncodePassword(user);

                user.Updated = user.Created;
                user.LastLogin = user.Created;
                user.LastActivity = user.Created;

                domainModel.Users.Add(user);

                domainModel.SaveChanges();

                status = MembershipCreateStatus.Success;
                return ToMembershipUser(user);
            }
        }

        private static HashAlgorithm CreateHashAlgorithm()
        {
            return HashAlgorithm.Create(Membership.HashAlgorithmType);
        }

        private static string GenerateSalt()
        {
            var bytes = new byte[SaltLength];
            new RNGCryptoServiceProvider().GetBytes(bytes);
            return Convert.ToBase64String(bytes);
        }

        private string DecodePassword(string password)
        {
            if (string.IsNullOrEmpty(password))
            {
                throw new NotSupportedException("Password cannot be empty");
            }

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Hashed:
                    throw new NotSupportedException("Cannot decode hashed password");

                default:
                    return password;

                case MembershipPasswordFormat.Encrypted:
                {
                    var encodedPassword = Convert.FromBase64String(password);
                    var decodedBlock = DecryptPassword(encodedPassword);
                    if (decodedBlock == null)
                    {
                        return null;
                    }
                    return Encoding.Unicode.GetString(decodedBlock, 0, decodedBlock.Length - SaltLength);
                }
            }
        }

        private string GetPasswordHash(string password, string salt)
        {
            var hashAlgorithm = CreateHashAlgorithm();
            var passwordBytes = Encoding.Unicode.GetBytes(password);
            var saltBytes = Convert.FromBase64String(salt);
            if (hashAlgorithm is KeyedHashAlgorithm)
            {
                var keyedHashAlgorithm = hashAlgorithm as KeyedHashAlgorithm;

                var key = saltBytes;

                if (key.Length != keyedHashAlgorithm.Key.Length)
                {
                    var source = key;
                    key = new byte[keyedHashAlgorithm.Key.Length];
                    Buffer.BlockCopy(source, 0, key, 0, Math.Min(key.Length, source.Length));
                }

                keyedHashAlgorithm.Key = key;

                return Convert.ToBase64String(keyedHashAlgorithm.ComputeHash(passwordBytes));
            }
            else
            {
                var key = new byte[passwordBytes.Length + saltBytes.Length];
                Buffer.BlockCopy(passwordBytes, 0, key, 0, passwordBytes.Length);
                Buffer.BlockCopy(saltBytes, 0, key, passwordBytes.Length, saltBytes.Length);
                return Convert.ToBase64String(hashAlgorithm.ComputeHash(key));
            }
        }

        private void EncodePassword(User user)
        {
            if (string.IsNullOrEmpty(user.Password))
            {
                throw new NotSupportedException("Password cannot be empty");
            }

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Hashed:
                {
                    user.Salt = GenerateSalt();
                    user.Password = GetPasswordHash(user.Password, user.Salt);
                    break;
                }

                case MembershipPasswordFormat.Encrypted:
                {
                    var passwordBytes = Encoding.Unicode.GetBytes(user.Password);
                    var saltBytes = Convert.FromBase64String(GenerateSalt());
                    var key = new byte[passwordBytes.Length + saltBytes.Length];
                    Buffer.BlockCopy(passwordBytes, 0, key, 0, passwordBytes.Length);
                    Buffer.BlockCopy(saltBytes, 0, key, passwordBytes.Length, saltBytes.Length);
                    var encryptedPassword = EncryptPassword(key);
                    user.Password = Convert.ToBase64String(encryptedPassword);
                    break;
                }
            }
        }

        private MembershipUser ToMembershipUser(User user)
        {
            return new MembershipUser(Name, user.Name, 
                user.ProviderUserKey.HasValue ? (object)user.ProviderUserKey.Value : null, user.Email, user.PasswordQuestion, 
                user.Comment, user.IsApproved, user.IsLockedOut, user.Created, user.LastLogin, user.LastActivity, 
                user.LastPasswordChange ?? DateTime.MinValue, user.LastLockout ?? DateTime.MinValue);
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username && u.Password == password);

                if (user == null)
                {
                    return false;
                }

                user.PasswordQuestion = newPasswordQuestion;
                user.PasswordAnswer = newPasswordAnswer;
                domainModel.SaveChanges();

                return true;
            }
        }

        public override string GetPassword(string username, string answer)
        {
            if (!EnablePasswordRetrieval)
            {
                throw new NotSupportedException("Password retrieval is not supported");
            }

            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username && u.PasswordAnswer == answer);

                return user != null ? DecodePassword(user.Password) : null;
            }
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            ValidatePassword(newPassword);

            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username && u.Password == oldPassword);

                if (user == null)
                {
                    return false;
                }

                user.Password = newPassword;
                EncodePassword(user);
                domainModel.SaveChanges();
                return true;
            }
        }

        public override string ResetPassword(string username, string answer)
        {
            if (!EnablePasswordReset)
            {
                throw new NotSupportedException("Password reset is not supported");
            }

            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username && u.PasswordAnswer == answer);

                if (user == null)
                {
                    return null;
                }

                var password = Membership.GeneratePassword(MinRequiredPasswordLength, MinRequiredNonAlphanumericCharacters);

                user.Password = password;
                EncodePassword(user);

                domainModel.SaveChanges();

                return password;
            }
        }

        public override void UpdateUser(MembershipUser source)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == source.UserName);

                if (user == null)
                {
                    return;
                }

                user.Email = source.Email;
                user.IsApproved = source.IsApproved;
                user.Comment = source.Comment;
                user.LastActivity = source.LastActivityDate;
                user.LastLockout = source.LastLockoutDate;
                user.LastLogin = source.LastLoginDate;
                user.LastPasswordChange = source.LastPasswordChangedDate;

                domainModel.SaveChanges();
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            if (!string.IsNullOrEmpty(password))
            {
                password = password.Trim();
            }

            if (!string.IsNullOrEmpty(username))
            {
                username = username.Trim();
            }

            if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(username))
            {
                return false;
            }

            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username);

                if (user == null)
                {
                    return false;
                }

                var validated = false;

                switch (PasswordFormat)
                {
                    case MembershipPasswordFormat.Hashed:
                        validated = GetPasswordHash(password, user.Salt) == user.Password;
                        break;
                    case MembershipPasswordFormat.Clear:
                        validated = password == user.Password;
                        break;
                    case MembershipPasswordFormat.Encrypted:
                        validated = DecodePassword(user.Password) == password;
                        break;
                }

                if (!validated)
                {
                    user.IncorrectLoginAttempts++;
                    domainModel.SaveChanges();
                    return false;
                }

                user.IncorrectLoginAttempts = 0;
                user.LastLogin = DateTime.UtcNow;
                user.LastActivity = DateTime.UtcNow;
                domainModel.SaveChanges();
                return true;
            }
        }

        public override bool UnlockUser(string userName)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == userName);

                if (user == null)
                {
                    return false;
                }

                user.IsApproved = true;
                domainModel.SaveChanges();
                return true;
            }
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            if (providerUserKey == null)
            {
                throw new ArgumentNullException("providerUserKey");
            }

            if (!(providerUserKey is Guid))
            {
                throw new ArgumentException("Invalid provider user key", "providerUserKey");
            }

            using (var domainModel = new DataAccess.DomainModel())
            {
                var key = (Guid) providerUserKey;

                var user = domainModel.Users.FirstOrDefault(u => u.ProviderUserKey.HasValue & u.ProviderUserKey.Value == key);

                if (user == null)
                {
                    return null;
                }

                if (userIsOnline)
                {
                    UpdateUserOnline(user);
                    domainModel.SaveChanges();
                }

                return ToMembershipUser(user);
            }
        }

        private static void UpdateUserOnline(User user)
        {
            user.LastActivity = DateTime.UtcNow;
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username);

                if (user == null)
                {
                    return null;
                }

                if (userIsOnline)
                {
                    UpdateUserOnline(user);
                    domainModel.SaveChanges();
                }

                return ToMembershipUser(user);
            }
        }

        public override string GetUserNameByEmail(string email)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                return domainModel.Users.Where(u => u.Email == email).Select(u => u.Name).FirstOrDefault();
            }
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var user = domainModel.Users.FirstOrDefault(u => u.Name == username);

                if (user == null)
                {
                    return false;
                }

                domainModel.Users.Remove(user);
                domainModel.SaveChanges();

                return true;
            }
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var collection = new MembershipUserCollection();
                foreach (var user in domainModel.Users.Skip((pageIndex - 1)*pageSize).Take(pageSize))
                {
                    collection.Add(ToMembershipUser(user));
                }
                totalRecords = domainModel.Users.Count();
                return collection;
            }
        }

        public override int GetNumberOfUsersOnline()
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                return domainModel.Users.Count(u => u.LastActivity > DateTime.UtcNow - TimeSpan.FromMinutes(Membership.UserIsOnlineTimeWindow));
            }
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var collection = new MembershipUserCollection();
                var users = domainModel.Users.Where(u => u.Name.Contains(usernameToMatch));
                totalRecords = users.Count();
                foreach (var user in users.Skip((pageIndex-1)*pageSize).Take(pageSize))
                {
                    collection.Add(ToMembershipUser(user));
                }
                return collection;
            }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            using (var domainModel = new DataAccess.DomainModel())
            {
                var collection = new MembershipUserCollection();
                var users = domainModel.Users.Where(u => u.Email.Contains(emailToMatch));
                totalRecords = users.Count();
                foreach (var user in users.Skip((pageIndex - 1) * pageSize).Take(pageSize))
                {
                    collection.Add(ToMembershipUser(user));
                }
                return collection;
            }
        }

        public override bool EnablePasswordRetrieval
        {
            get { return _enablePasswordRetrieval; }
        }

        public override bool EnablePasswordReset
        {
            get { return _enablePasswordReset; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return _requiresQuestionAndAnswer; }
        }

        public override string ApplicationName { get; set; }

        public override int MaxInvalidPasswordAttempts
        {
            get { return _maxInvalidPasswordAttempts; }
        }

        public TimeSpan PasswordAttemptPeriod
        {
            get { return TimeSpan.FromMinutes(PasswordAttemptWindow); }
        }

        public override int PasswordAttemptWindow
        {
            get { return _passwordAttemptWindow; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return _requiresUniqueEmail; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _passwordFormat; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _minRequiredNonAlphanumericCharacters; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return _passwordStrengthRegularExpression; }
        }
    }
}